Dynamic Data Exchange

0x1 Dynamic Data Exchange

0x1.1 什么是 Dynamic Data Exchange

      Dynamic Data Exchange(DDE),译为动态数据交换。DDE是一种Inter-Process Communication机制下的一种客服端服务器协议。

      在MicroSoft Office 2016 之前的Office版本是默认开启的,但是随着DDE功能的滥用,在2017年,微软安全团队已经禁用Word下的DDE功能,但是仍然留有方式,以便用户正常的开启DDE功能,即通过如下注册表来设置是否启用DDE功能。具体详情可以在Microsoft Disables DDE Feature in Word to Prevent Further Malware Attacks看到

1
2
3
4
\HKEY_CURRENT_USER\Software\Microsoft\Office\version\Word\Security AllowDDE(DWORD)
AllowDDE(DWORD)= 0:禁用DDE。安装更新后,这是默认设置。
AllowDDE(DWORD)= 1:允许对已经运行的程序进行DDE请求,但阻止需要启动另一个可执行程序的DDE请求。
AllowDDE(DWORD)= 2:完全允许DDE请求。

      DDE和宏是不同的两种技术,通过嵌入DDE命令是不会被扫描到存在宏代码的。本文行文仓促,如有错误,请各位积极指正。

0x1.2 Dynamic Data Exchange 的利用

      早在2017年,William在他的文章Office DDEAUTO attacks中就详细描述了在Word,Outlook,Calendar Invites上的利用。并在文章的末尾标明了一些会导致DDE被滥用的文件格式:doc(x/m)、dot(x/m)、rtf、Word XML。

      在Office Word中,William按下CTRL+F9很轻易的构造了一个含有DDE的word文档。如图所示,在花括号中,输入DDEAUTO c:\windows\system32\cmd.exe "/k notepad.exe,然后保存即可。当然因为doc,docx,rtf都是使用office word打开的,所以可以保存三种不同格式的文档。
mark

      这样,我们就构造了doc,docx,rtf三种带有DDE的文档。打开之后应该就是下面这个样子。但是我们怎么能查看到这些文档中带有的恶意代码呢?首先保证你有一个分析环境,保证样本所有的恶意行为都在你的掌握之中,这样就不会因为自己的手残点了一个是,造成恶劣的后果。
mark

      因为Word会提醒你是否需要更新,记住,全部点否即可。如此的话,就可以看到这样的情况。这只是测试.doc,如果遇上真实的样本,肯定有着极其丰富的内容,肯定是不容易找到的。在Word光标附近,右键,切换域代码即可看到恶意代码的内容。
mark
mark

      通常,在excel中,使用=DDE()公式的方式构造带有DDE功能的文档,在2014年,Contextis就公开了他的研究。如下图所示,直接在空格里面输入=dde(cmd|'/k calc.exe'!.A1)即可构造和上面功能一样的excel文件,同理,除了可以生成xls文件外,还可以生成xlsx文件。
mark

      打开之后呢,就是这样的效果,但是修改之后的痕迹很容易被发现,所以还需要有一些社会工程学的技巧尽可能的隐藏这样的痕迹。
mark

      接下来就是如何构造含有DDE的ppt和pptx文档。网上找了好多也没有找到如何构造PowerPoint的DDE的方法。但是微软的这句话给了我一丝灵感PowerPoint can import an outline in .docx, .rtf, or .txt format.,翻译过来的意思就是PowerPoint可以导入.docx,.rtf或.txt格式的大纲。这让我想到William在构造Outlook,以及OneNote也是用这样的方法进行DDE的。本身我们已经有了携带有DDE功能的docx或者rtf,只要我们将这些文件作为对象插入PowerPoint中,那就相当于构造了一个携带有dde功能的powerpoint文档了。说干就干。在PowerPoint中选择插入对象选项卡,并浏览已经构造好的docx文件。这样就可以了。然后点击插入的docx文件就可以弹出Notepad.exe了。
mark

0x1.2 如何检测 Dynamic Data Exchange

      在常见的Office组件中,Word,Excel,PPT文档主要的就是两种格式,一种是带有拓展的,一种是不带有拓展的。其中带有拓展的是在Office2007及其以后的版本使用的文档规范,称之为OpenXML,这种文档采用XML组织起文件结构。另外一种格式就是OLE,即对象链接和嵌入技术,这种并不是采用xml组织文件结构,而是采用类似于电脑文件存储的方式组织。而OpenXMl从文件大小上来看也比OLE文件要小得多。并且可以通过压缩软件打开。关于OpenXML和OLE文件可以翻阅微软文档或者Office文件格式基础知识(1)
mark

      OpenXML具有很好的辨识度,可以直接查看存储的内容,编写出检测代码也比较方便。先为各位介绍OpenXML文档的识别与检测。

      之前说了OpenXML都是可以使用压缩软件打开,例如这样的。其中DDE代码就存在\word\document.xml文件中。通过搜索关键字’cmd’,可以看到带有DDE功能的代码都在tag=’r’以及tag=’instrText’中。我们就可以以此作为检测依据。而针对XML文件,可以使用ElementTree进行解析,ElementTree提供一个深度遍历所有节点的函数——iter(),利用它就可以遍历所有的节点,只要当前节点的tag为’r’,以及存在tag为’instrText’的子节点,那么’instrText’的内容就是DDE代码。
mark
mark

1
2
3
4
5
6
7
8
9
10
tree = ET.parse(self.zipper.open("word/document.xml"))
for elem in tree.iter():
if elem.tag.split('}')[1] =='r':
for sub in elem:
if sub.tag.split('}')[1] == "instrText":
text = sub.text.strip().replace('\n', '').replace('\r', '')
if "QUOTE" in text:
text = self.clean_quote(text)
self.ddetext += text
self.zipper.close()

      又因为w="http://schemas.openxmlformats.org/wordprocessingml/2006/main",所以需要截取’}’后面的那部分内容,这个大家做个试验,打印一下elem.tag的内容就清楚了。

      对于xlsx文件呢,dde代码是存储在\xl\externalLinks目录下的xml文件中,每一个链接对应着不同的xml文件,比如我构造的excel文档,他有两个链接,他既可以执行calc.exe 又可以执行notepad.exe。所以在\xl\externalLinks目录下面存在两个不同的xml文件。在Link1.xml中,当tag为’ddeLink’说明存在dde代码,其中ddeService=”cmd”。ddeTopic=”/k calc.exe”。同理,只需要遍历xml所有节点,找到ddeLink即可检测。
mark
mark
mark

1
2
3
4
5
6
7
8
tree = ET.parse(self.zipper.open("xl/externalLinks/externalLink1.xml"))
for elem in tree.iter():
if elem.tag.split('}')[1] =='ddeLink':
if "ddeService" in elem.attrib.keys() and "ddeTopic" in elem.attrib.keys():
if elem.attrib["ddeService"]:
self.ddetext += elem.attrib["ddeService"]
if elem.attrib["ddeTopic"]:
self.ddetext += elem.attrib["ddeTopic"]

      而针对PPTX呢,因为PPTX是将其他dde文档作为对象插入的,所以只需要提取出那些dde文档,然后针对性的检测那部分文档即可。插入的文档位于\ppt\embeddings目录中。可以利用zipper的.extract()方法将目录下的文件提取到指定目录,然后检测该文件是否存在dde代码即可。

1
2
3
4
5
6
7
8
9
zipper = zipfile.ZipFile(filename)
subfiles = zipper.namelist()
for subfile in subfiles:
if targetdir in subfile:
zipper.extract(subfile,"C:/Users/Public/",None)
zipper.close()
print(os.path.join("C:/Users/Public/",subfile))
return os.path.join("C:/Users/Public/",subfile)
return None

      除了这三种OpenXML格式呢,rtf富文本格式也是明文存在的。如图,是一个rtf文件的内容,rtf文件的Magic标志位为’{\rt’,在测试的例子中,我们搜索’cmd.exe’,显然可以找到我们编写的dde代码。【图14,图15】
mark
mark

      在rtf文件中,是以’{‘和’}’作为分界的。并且空格在文件中无意义。这些我们提取出可以作为检测条件的部分如下,可以看到’\field’和’\fldrslt’,以及’fldinst’可以作为检测的Key,只需要检测到这三个部分,并将’\fldinst’中间的这些属性排除,就可以得到dde代码了。是不是很简单

1
2
{\field{\*\fldinst {\rtlch\fcs1 \af13 \ltrch\fcs0 \cf19\loch\af44\hich\af44\dbch\af44\chshdng0\chcfpat0\chcbpat8\insrsid14360340 \hich\af44\dbch\af44\loch\f44 DDEAUTO c:
\\\\windows\\\\system32\\\\cmd.exe "/k notepad.exe" }}{\fldrslt }}

      DOC文档采用的是OLE格式,我们可以简单的将OLE理解成硬盘的文件系统,将整个文件理解成一个磁盘分区,磁盘里面有很多个文件或者文件夹,把文件夹理解成仓库(Storages),将文件理解成流(Stream)。

      可以使用OffVis工具来解析OLE文件,OffVis可以解析OLE类型的Word,Excel,PowerPoint文件。可以看到OffVis将dde_test.doc文件解析成4部分【图16】:
mark

  • OleHeader:符合文档头,其大小为0x200个字节,请各位记住这个数字,其存储了整个文档的诸多关键信息。
  • FAT[]:这是一个数组,里面存储着诸多的Sectors,Sectors称之为数据扇区。
  • MiniFAT:暂时没用到
  • DirectoryEntry:目录入口,可以简单理解目录是寻找文件或者文件夹的入口,因为每一个目录入口都指向复合文档的一个仓库或流。

      这是OLE文件DirectoryEntry结构的内容,其中,我们需要关注的是如下几个属性【图17】
mark

  • EleName:表示目录名称
  • Type:表示目录的类型,其中Type=2,说明这是一个Stream。
  • StartSect:表示第一个Sector的编号,可以表征该目录对应数据的Offset,其计算公式应为:0x200+StartSect*SizeOfSector
  • SizeLow:表示大小

      在OffVis中搜索“cmd.exe”,发现dde代码位于0xA00处,经过分析,可以发现,这段数据位于第3号DirectoryEntry指向的数据。而这样第3号DirectoryEntry的类型为Stream,我们就可以将寻找dde的过程提取总结出来,即【图18】:
mark

  • 首先解析DirectoryEntry[],寻找其中Type=Stream(2)的DirectoryEntry
  • 然后获取该DirectoryEntry的StartSect和SizeLow,通过这两个成员,获取DirectoryEntry指向的数据。
  • 接着遍历这些数据,这些数据有3种不同的特征数据,分别是FIELD_START(0x13),FIELD_SEP(0x14),FIELD_END(0x15),如下图,很显然,dde代码是位于FIELD_START和FIELD_SEP以及FIELD_END之间。即检测到了FIELD_START,但是没有检测到FIELD_SEP和FIELD_END,并且数据是可见字符。这样便可以提取dde代码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    while True:
    # index increases 1 when every traversal
    index += 1
    char = stream.read(1)
    # char is None means end of stream
    if len(char) ==0:
    break
    else:
    char = ord(char)
    # Start
    if char == OLE_FIELD_START:
    is_start = True
    is_sep = False
    field_contents = u''
    continue
    elif is_start == False:
    continue
    # Step
    if char == OLE_FIELD_SEP:
    is_sep = True
    # End
    if char == OLE_FIELD_END:
    if field_contents:
    field_contents = self.process_doc_field(data=field_contents)
    if field_contents:
    dde_result.append(field_contents)
    is_end = True
    is_start = False
    is_sep = False
    field_contents = None
    if is_start == True and is_sep == False and is_end == False:
    if char > 31 and char < 127:
    field_contents += unichr(char)

      关于OLE复合文档格式,可以在StriveMario的复合文档Ole对象二进制储存格式一文中,找到非常详细的介绍。

      xls文档和doc文档在寻找DDE的逻辑上是一致的,首先遍历DirectoryEntry寻找Type为Stream的那一个,然后遍历其中的Record。寻找Type为430的那一个Record的。然后在解析Record的Data内容即可。从xls_parser.py中可以看到,当ctab为0x00,且存在virt_path的时候,说明link的类型为LINK_TYPE_OLE_DDE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# parse data
if self.size < 4:
raise ValueError('not enough data (size is {0} but need >= 4)'
.format(self.size))
self.ctab, self.cch = unpack('<HH', self.data[:4])
if 0 < self.cch <= 0xff:
# this is the length of virt_path
self.virt_path, _ = read_unicode(self.data, 4, self.cch)
else:
self.virt_path, _ = u'', 4
# ignore variable rgst
if self.cch == 0x401: # ctab is undefined and to be ignored
self.support_link_type = self.LINK_TYPE_SELF
elif self.ctab == 0x1 and self.cch == 0x3A01:
self.support_link_type = self.LINK_TYPE_ADDIN
# next records must be ExternName with all add-in functions
elif self.virt_path == u'\u0020': # space ; ctab can be anything
self.support_link_type = self.LINK_TYPE_UNUSED
elif self.virt_path == u'\u0000':
self.support_link_type = self.LINK_TYPE_SAMESHEET
elif self.ctab == 0x0 and self.virt_path:
self.support_link_type = self.LINK_TYPE_OLE_DDE
elif self.ctab > 0 and self.virt_path:
self.support_link_type = self.LINK_TYPE_EXTERNAL

0x1.3 后记

      paloalto在Sofacy Group’s Parallel Attacks这篇文章提到可以理由QUOTE字段进行混淆。为了对抗基于检测DDE的检测方法,也可以构造一个不含有‘DDE’的文档。大家可以在MSWord - Obfuscation with Field Codes这篇文章中看到。这两部分内容我不准备继续深入下去了。因为苦逼的打工人要去找房子了,呜呜呜。